<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="class.html">
<link rel="import" href="../../polymer.html">
<link rel="import" href="../utils/mixin.html">
<link rel="import" href="../utils/templatize.html">

<script>
(function() {
  'use strict';

  const UndefinedArgumentError = class extends Error {
    constructor(message, arg) {
      super(message);
      this.arg = arg;
      this.name = this.constructor.name;
      // Affordances for ensuring instanceof works after babel ES5 compilation
      // TODO(kschaaf): Remove after polymer CLI updates to newer Babel that
      // sets the constructor/prototype correctly for subclassed builtins
      this.constructor = UndefinedArgumentError;
      this.__proto__ = UndefinedArgumentError.prototype;
    }
  };

  /**
   * Wraps effect functions to catch `UndefinedArgumentError`s and warn.
   * 
   * @param {Object=} effect Effect metadata object
   * @param {Object=} fnName Name of user function, if known
   * @return {?Object} Effect metadata object
   */
  function wrapEffect(effect, fnName) {
    if (effect && effect.fn) {
      const fn = effect.fn;
      effect.fn = function() {
        try {
          fn.apply(this, arguments);
        } catch (e) {
          if (e instanceof UndefinedArgumentError) {
            console.warn(`Argument '${e.arg}'${fnName ?` for method '${fnName}'` : ''} was undefined. Ensure it has an undefined check.`);
          } else {
            throw e;
          }
        }
      };
    }
    return effect;
  }
  
  /**
   * Mixin to selectively add back Polymer 1.x's `undefined` rules
   * governing when observers & computing functions run based
   * on all arguments being defined (reference https://www.polymer-project.org/1.0/docs/devguide/observers#multi-property-observers).
   * 
   * When loaded, all legacy elements (defined with `Polymer({...})`)
   * will have the mixin applied. The mixin only restores legacy data handling
   * if `_legacyUndefinedCheck: true` is set on the element's prototype.
   * 
   * This mixin is intended for use to help migration from Polymer 1.x to
   * 2.x+ by allowing legacy code to work while identifying observers and
   * computing functions that need undefined checks to work without
   * the mixin in Polymer 2.
   *
   * @mixinFunction
   * @polymer
   * @summary Mixin to selectively add back Polymer 1.x's `undefined` rules
   * governing when observers & computing functions run.
   */
  Polymer.LegacyDataMixin = Polymer.dedupingMixin(superClass => {

    /**
     * @polymer
     * @mixinClass
     * @implements {Polymer_LegacyDataMixin}
     */
    class LegacyDataMixin extends superClass {
      /**
       * Overrides `Polyer.PropertyEffects` to add `undefined` argument
       * checking to match Polymer 1.x style rules
       * 
       * @param {!Array<!MethodArg>} args Array of argument metadata
       * @param {string} path Property/path name that triggered the method effect
       * @param {Object} props Bag of current property changes
       * @return {Array<*>} Array of argument values
       * @private
       */
      _marshalArgs(args, path, props) {
        const vals = super._marshalArgs(args, path, props);
        // Per legacy data rules, single-property observers (whether in `properties`
        // and in `observers`) are called regardless of whether their argument is
        // undefined or not. Multi-property observers must have all arguments defined
        if (this._legacyUndefinedCheck && vals.length > 1) {
          for (let i=0; i<vals.length; i++) {
          if (vals[i] === undefined || 
              (args[i].wildcard && vals[i].base === undefined)) {
              // Break out of effect's control flow; will be caught in
              // wrapped property effect function below
              const name = args[i].name;
              throw new UndefinedArgumentError(`Argument '${name}' is undefined. Ensure it has an undefined check.`, name);
            }
          }
        }
        return vals;
      }

      /**
       * Overrides `Polyer.PropertyEffects` to wrap effect functions to
       * catch `UndefinedArgumentError`s and warn.
       * 
       * @param {string} property Property that should trigger the effect
       * @param {string} type Effect type, from this.PROPERTY_EFFECT_TYPES
       * @param {Object=} effect Effect metadata object
       * @return {void}
       * @protected
       */
      _addPropertyEffect(property, type, effect) {
        return super._addPropertyEffect(property, type,
          wrapEffect(effect, effect && effect.info && effect.info.methodName));
      }

      /**
       * Overrides `Polyer.PropertyEffects` to wrap effect functions to
       * catch `UndefinedArgumentError`s and warn.
       *
       * @param {Object} templateInfo Template metadata to add effect to
       * @param {string} prop Property that should trigger the effect
       * @param {Object=} effect Effect metadata object
       * @return {void}
       * @protected
       */
      static _addTemplatePropertyEffect(templateInfo, prop, effect) {
        return super._addTemplatePropertyEffect(templateInfo, prop, wrapEffect(effect));
      }

    }

    return LegacyDataMixin;

  });

  // LegacyDataMixin is applied to base class _before_ metaprogramming, to
  // ensure override of _addPropertyEffect et.al. are used by metaprogramming
  // performed in _finalizeClass
  const Class = Polymer.Class;
  Polymer.Class = (info, mixin) => Class(info, 
    superClass => mixin ? 
      mixin(Polymer.LegacyDataMixin(superClass)) : 
      Polymer.LegacyDataMixin(superClass)
  );

  // Apply LegacyDataMixin to Templatizer instances as well, and defer
  // runtime switch to the root's host (_methodHost)
  Polymer.Templatize.mixin = 
    Polymer.dedupingMixin(superClass => class extends Polymer.LegacyDataMixin(superClass) {
      get _legacyUndefinedCheck() {
        return this._methodHost && this._methodHost._legacyUndefinedCheck;
      }
    });

  console.info('LegacyDataMixin will be applied to all legacy elements.\n' +
               'Set `_legacyUndefinedCheck: true` to enable.');

})();
</script>
